<head>
    <style>
        .pass, .fail {
            height: 30px;
            line-height: 30px;
            color: white;
            display: block;
            border: 1px solid black;
            margin: 3px;
            padding: 3px;
        }

        .pass { background-color: green; }
        .fail { background-color: red; }
    </style>
</head>
<body>
    <div id=test0></div>
    <div id=test1></div>
    <div id=test2></div>

    <script>
        let test0 = document.getElementById('test0');
        let test1 = document.getElementById('test1');
        let test2 = document.getElementById('test2');

        const bindMethodsForTest = (method) => {
            let failures = 0;

            const pass = (message) => {
                if (failures > 0)
                    return;

                const span = document.createElement('span');
                span.innerHTML = `Pass! ${method}`;
                span.setAttribute('class', 'pass');
                document.body.appendChild(span);

                test0.className = '';
                test1.className = '';
                test2.className = '';
            };

            const fail = (message) => {
                const span = document.createElement('span');
                span.innerHTML = `Fail! ${method}: ${message}`;
                span.setAttribute('class', 'fail');
                document.body.appendChild(span);
                ++failures;
            };

            const invalidTokenTest = () => {
                const test = (args1, args2) => {
                    try {
                        test0.classList[method](...args1);
                        fail(`Expected classList.${method}(${args1}) to throw due to empty token`);
                    } catch (error) {
                        if (error.name !== 'SyntaxError')
                            fail(`Expected exception to be a DOMException of type SyntaxError but is "${error.name}"`);
                    }

                    try {
                        test0.classList[method](...args2);
                        fail(`Expected classList.${method}(${args2}) to throw due to token containing a space`);
                    } catch (error) {
                        if (error.name !== 'InvalidCharacterError')
                            fail(`Expected exception to be a DOMException of type InvalidCharacterError but is "${error.name}"`);
                    }
                };

                if (test0.classList[method].length === 2) {
                    test(['', 'foo'], ['foo bar', 'foobar']);
                    test(['foo', ''], ['foobar', 'foo bar']);
                } else {
                    test([''], ['foo bar']);
                }
            };

            return [pass, fail, invalidTokenTest];
        };

        (() => {
            const [pass, fail] = bindMethodsForTest('contains');

            test0.className = 'foo';
            test1.className = 'foo bar';

            if (!test0.classList.contains('foo'))
                fail('test0 should contain class "foo"');
            if (test0.classList.contains('bar'))
                fail('test0 should not contain class "bar"');

            if (!test1.classList.contains('foo'))
                fail('test1 should contain class "foo"');
            if (!test1.classList.contains('bar'))
                fail('test1 should contain class "bar"');

            pass();
        })();

        (() => {
            const [pass, fail, invalidTokenTest] = bindMethodsForTest('add');
            invalidTokenTest();

            test0.classList.add('foo');
            test1.classList.add('foo', 'bar');
            test2.classList.add('foo', 'bar', 'foo');

            if (test0.className !== 'foo')
                fail(`test0 should have a class name of "foo" but has "${test0.className}"`)
            if (test1.className !== 'foo bar')
                fail(`test1 should have a class name of "foo bar" but has "${test1.className}"`)
            if (test2.className !== 'foo bar')
                fail(`test2 should have a class name of "foo bar" but has "${test2.className}"`)

            pass();
        })();

        (() => {
            const [pass, fail, invalidTokenTest] = bindMethodsForTest('remove');
            invalidTokenTest();

            test0.className = 'foo';
            test1.className = 'foo bar';
            test2.className = 'foo bar';

            test0.classList.remove('foo');
            test1.classList.remove('foo', 'bar');
            test2.classList.remove('bar');

            if (test0.className !== '')
                fail(`test0 should have a class name of "" but has "${test0.className}"`)
            if (test1.className !== '')
                fail(`test1 should have a class name of "" but has "${test1.className}"`)
            if (test2.className !== 'foo')
                fail(`test2 should have a class name of "foo" but has "${test2.className}"`)

            pass();
        })();

        (() => {
            const [pass, fail, invalidTokenTest] = bindMethodsForTest('toggle');
            invalidTokenTest();

            if (!test0.classList.toggle('foo'))
                fail('test0 should contain class "foo"');
            if (!test0.classList.toggle('bar', true))
                fail('test0 should contain class "bar"');
            if (test0.classList.toggle('baz', false))
                fail('test0 should not contain class "baz"');

            if (test0.classList.toggle('foo'))
                fail('test0 should not contain class "foo"');
            if (!test0.classList.toggle('bar', true))
                fail('test0 should contain class "bar"');
            if (test0.classList.toggle('bar', false))
                fail('test0 should not contain class "bar"');

            pass();
        })();

        (() => {
            const [pass, fail, invalidTokenTest] = bindMethodsForTest('replace');
            invalidTokenTest();

            if (test0.classList.replace('foo', 'bar'))
                fail('test0 should not have contained class "bar"');
            if (test0.className !== '')
                fail(`test0 should have a class name of "" but has "${test0.className}"`)

            test0.className = 'foo';

            if (!test0.classList.replace('foo', 'bar'))
                fail('test0 should have contained class "bar"');
            if (test0.className !== 'bar')
                fail(`test0 should have a class name of "bar" but has "${test0.className}"`)

            test0.className = 'foo bar';

            if (!test0.classList.replace('bar', 'baz'))
                fail('test0 should have contained class "bar"');
            if (test0.className !== 'foo baz')
                fail(`test0 should have a class name of "foo baz" but has "${test0.className}"`)

            test0.className = 'foo bar baz';

            if (!test0.classList.replace('foo', 'baz'))
                fail('test0 should have contained class "bar"');
            if (test0.className !== 'baz bar')
                fail(`test0 should have a class name of "baz bar" but has "${test0.className}"`)

            test0.className = 'baz bar foo';

            if (!test0.classList.replace('foo', 'baz'))
                fail('test0 should have contained class "bar"');
            if (test0.className !== 'baz bar')
                fail(`test0 should have a class name of "baz bar" but has "${test0.className}"`)

            test0.className = 'foo bar baz';

            if (!test0.classList.replace('baz', 'baz'))
                fail('test0 should have contained class "bar"');
            if (test0.className !== 'foo bar baz')
                fail(`test0 should have a class name of "foo bar baz" but has "${test0.className}"`)

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('supports');

            try {
                test0.classList.supports('foo');
                fail('Expected supports to throw due to no defined supported tokens');
            } catch (error) {
                if (error.name !== 'TypeError')
                    fail(`Expected exception to be a DOMException of type TypeError but is "${error.name}"`);
            }

            pass();
        })();

        (() => {
            const [pass, fail] = bindMethodsForTest('value');

            test0.classList.value = 'foo';

            if (test0.className !== 'foo')
                fail(`test0 should have a class name of "foo" but has "${test0.className}"`)
            if (test0.classList.value !== 'foo')
                fail(`test0 should have a class name of "foo" but has "${test0.className}"`)

            test0.classList.value = 'foo bar';

            if (test0.className !== 'foo bar')
                fail(`test0 should have a class name of "foo bar" but has "${test0.className}"`)
            if (test0.classList.value !== 'foo bar')
                fail(`test0 should have a class name of "foo bar" but has "${test0.className}"`)

            pass();
        })();
    </script>
</body>
